﻿using System;
using System.Collections.Generic;
using System.Linq;
using NthDownload.Common.Plugins;
using NthDownload.Contracts;
using System.ComponentModel;
using System.IO;
using NthDownload.Agent.EventArguments;
using NthDownload.Agent.Exceptions;
using System.Windows.Threading;
using System.Threading.Tasks;
using NthDownload.Common.Utils;
using NthDownload.Contracts.Plugins.Downloads;
using NthDownload.Contracts.Plugins.BandwidthProviders;
using NthDownload.Composition;

namespace NthDownload.Agent
{
    /// <summary>
    /// Processes 
    /// </summary>
    public class DownloadProcessor : INotifyPropertyChanged
    {
        #region Events

        #region Started

        /// <summary>
        /// Occurs when the processor starts downloading
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> Started;

        /// <summary>
        /// Invoke the Started event & update with started info
        /// </summary>
        protected void OnStarted()
        {
            //
            // Update download
            //
            Download.Status = DownloadStatuses.Downloading;
            SubscribeToElapsedTimer();

            //
            // Invoke event
            //
            if (Started != null)
            {
                Started.Invoke(this, new DownloadOperationCompletedEventArgs(Download, StateObject, null, false, false));
            }

            //
            // Clear the state for the current operation
            //
            StateObject = null;
        }

        #endregion Started

        #region Paused

        /// <summary>
        /// Occurs when the prcessor is has been succesfully paused
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> Paused;

        /// <summary>
        /// Invoke the Paused event & update with paused info
        /// </summary>
        protected void OnPaused()
        {
            //
            // Update download
            //
            Download.Status = DownloadStatuses.Paused;
            UpdateDownloadedAmount(0, true);
            UnsubscribeToElapsedTimer();

            //
            // Invoke event
            //
            if (Paused != null)
            {
                Paused.Invoke(this, new DownloadOperationCompletedEventArgs(Download, StateObject, null, false, false));
            }

            //
            // Clear the state for the current operation
            //
            StateObject = null;
        }

        #endregion Paused

        #region Stopped

        /// <summary>
        /// Occurs when the processor stops
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> Stopped;

        /// <summary>
        /// Invokes the Stopped event & update with stopped info
        /// </summary>
        protected void OnStopped(Exception errorException, bool aborted, bool canceled, DownloadStatuses stoppedStatus)
        {
            //
            // Update download
            //
            Download.Status = stoppedStatus;
            UpdateDownloadedAmount(0, true);
            UnsubscribeToElapsedTimer();

            //
            // Invoke Event
            //
            if (Stopped != null)
            {
                Stopped.Invoke(this, new DownloadOperationCompletedEventArgs(Download, StateObject, errorException, aborted, canceled));
            }

            //
            // Clear the state for the current operation
            //
            StateObject = null;
        }

        #endregion Stopped

        #region Finished

        /// <summary>
        /// Occurs when the processor finishes the download
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> Finished;

        /// <summary>
        /// Invoke the Finished event & update download with finish info
        /// </summary>
        protected void OnFinished()
        {
            //
            // Update download
            //
            Download.DateCompleted = DateTime.Now;
            Download.Status = DownloadStatuses.Finished;
            UpdateDownloadedAmount(0, true);
            UnsubscribeToElapsedTimer();

            //
            // Invoke event
            //
            if (Finished != null)
            {
                Finished.Invoke(this, new DownloadOperationCompletedEventArgs(Download, StateObject, null, false, false));
            }

            //
            // Clear the state for the current operation
            //
            StateObject = null;
        }

        #endregion Stopped

        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion INotifyPropertyChanged

        #endregion Events

        #region Class Members

        private const int DefualtChunkSize = 4096;
        private IDownload _download;
        private IBandwidthProvider _bandwidthProvider;
        private int _maxThreads;
        private int _maxRetries;

        #endregion Class Members

        #region Constructor

        /// <summary>
        /// Processes a download
        /// </summary>
        /// <param name="download">The download to process</param>
        /// <param name="bandwidthProver">The bandwidth provider to use</param>
        /// <param name="numberOfThreads">The number of threads to download on</param>
        /// <param name="maxNumberOfRetries">The maximum number of times the download can fail before the processor gives up</param>
        public DownloadProcessor(IDownload download, IBandwidthProvider bandwidthProver, int numberOfThreads, int maxNumberOfRetries)
        {
            DownloadedAmountLastUpdate = DateTime.Now;
            StopStatus = DownloadStatuses.Stopped;
            Download = download;
            BandwidthProvider = bandwidthProver;
            MaxThreads = numberOfThreads;
            MaxRetries = maxNumberOfRetries;
            OutstandingSegments = new List<DownloadSegment>();
            DownloadThreads = new Dictionary<DownloadSegment, DownloadThread>();
            StateObject = null;
        }

        static DownloadProcessor()
        {
            ElapsedTimerSubscriptionCount = 0;
            ElapsedTimer = new DispatcherTimer();
            ElapsedTimer.Interval = new TimeSpan(0, 0, 1);
        }

        #endregion Constructor

        #region Properties

        /// <summary>
        /// The download to be processed
        /// </summary>
        public IDownload Download
        {
            get
            {
                return _download;
            }
            private set
            {
                _download = value;
                OnPropertyChanged("Download");
            }
        }

        /// <summary>
        /// The bandwidth provider this processor will draw from
        /// </summary>
        public IBandwidthProvider BandwidthProvider
        {
            get
            {
                return _bandwidthProvider;
            }
            set
            {
                _bandwidthProvider = value;
                OnPropertyChanged("BandwidthProvider");
            }
        }

        /// <summary>
        /// The max number of threads to use for the download. If this is changed while the dowload is running
        /// the downloader will try and split what remains of the download into enough segments to satisfy 
        /// the max number of threads. If this isn't possible the excess threads will sit idle.
        /// </summary>
        public int MaxThreads
        {
            get
            {
                return Math.Max(_maxThreads, 1);
            }
            set
            {
                _maxThreads = Math.Max(value, 1);
                TryMatchDownloadThreads();
                OnPropertyChanged("MaxThreads");
            }
        }

        /// <summary>
        /// The maximum number of times the download processor can fail to initialize the download, open a stream and/or read from a stream
        /// before it gives up.
        /// </summary>
        public int MaxRetries
        {
            get
            {
                return Math.Max(_maxRetries, 1);
            }
            set
            {
                _maxRetries = Math.Max(value, 1);
            }
        }

        /// <summary>
        /// The chunk size to use for downloading, if the downloads chunk size is greater than zero it is returned,
        /// otherwise the default buffer size (4096) is returned.
        /// </summary>
        public int ChunkSize
        {
            get
            {
                return (Download.ChunkSize > 0) ? Download.ChunkSize : DefualtChunkSize;
            }
        }

        #endregion Properties

        #region Private Properties

        /// <summary>
        /// A list of the segments that haven't completed downloading
        /// </summary>
        private DateTime DownloadedAmountLastUpdate { get; set; }

        /// <summary>
        /// A buffer of the amount that has been downloaded since the last update of the DownloadedAmount property
        /// </summary>
        private long DownloadedAmountBuffer { get; set; }

        /// <summary>
        /// A list of the segments that haven't completed downloading
        /// </summary>
        private List<DownloadSegment> OutstandingSegments { get; set; }

        /// <summary>
        /// The active download threads
        /// </summary>
        private Dictionary<DownloadSegment, DownloadThread> DownloadThreads { get; set; }

        /// <summary>
        /// The status to set when a download is stopped
        /// </summary>
        private DownloadStatuses StopStatus { get; set; }

        /// <summary>
        /// State for the current operation
        /// </summary>
        private object StateObject { get; set; }

        /// <summary>
        /// Timer user to keep track of the elapsed times on downloads
        /// </summary>
        private static DispatcherTimer ElapsedTimer { get; set; }

        /// <summary>
        /// Holds the number of subscriptions to the ElapsedTimer Tick event.
        /// </summary>
        private static int ElapsedTimerSubscriptionCount { get; set; }

        #endregion Private Properties

        #region Methods

        /// <summary>
        /// Starts processing the download
        /// </summary>
        public void Start(object stateObject)
        {
            //
            // Check for pending operations
            //
            if (Download.Status == DownloadStatuses.CancelPending || Download.Status == DownloadStatuses.StopPending ||
                Download.Status == DownloadStatuses.PausePending)
            {
                throw new OperationPendingException("An operation is pending.");
            }

            //
            // Check the download is in a valid state
            //
            if (Download.Status == DownloadStatuses.Downloading)
            {
                throw new DownloadRunningException("Download already running.");
            }

            //
            // Record operation state
            //
            StateObject = stateObject;

            //
            // Initialize download
            //
            Task task = InitializeDownload();

            TaskScheduler ts = TaskScheduler.FromCurrentSynchronizationContext();
            Task continueTask = task.ContinueWith(t =>
                {
                    if (t.Exception == null)
                    {
                        //
                        // Create segments if needed
                        //
                        if (Download.Segments == null || Download.Segments.Count == 0)
                        {
                            CreateNewSegments();
                        }

                        //
                        // Get all outstanding segments form the download
                        //
                        PopulateOutstandingSegments();

                        //
                        // Create destination path if it is blank
                        //
                        if (string.IsNullOrWhiteSpace(Download.DestinationPath))
                        {
                            Download.DestinationPath = GetUniqueDestinationPath();
                        }

                        //
                        // Set status to downloading & raise started event
                        //
                        OnStarted();

                        //
                        // Create & start download threads
                        //
                        CreateDownloadThreads();
                    }
                    else
                    {
                        OnStopped(ExceptionUtils.GetInnerMostException(t.Exception), false, false, DownloadStatuses.Error);
                    }
                }, ts);

            continueTask.ContinueWith(t =>
                {
                    if (t.Exception == null)
                    {
                        //TODO Record error some where and provide feed back
                    }
                    else
                    {
                        OnStopped(ExceptionUtils.GetInnerMostException(t.Exception), false, false, DownloadStatuses.Error);
                    }
                }, ts);
        }

        /// <summary>
        /// Pauses processing of the download
        /// </summary>
        public void Pause(object stateObject)
        {
            //
            // Check for pending operations
            //
            if (Download.Status == DownloadStatuses.CancelPending || Download.Status == DownloadStatuses.StopPending ||
                Download.Status == DownloadStatuses.PausePending || StateObject != null)
            {
                throw new OperationPendingException("An operation is pending.");
            }

            //
            // Check the download is in a valid state
            //
            if (Download.Status != DownloadStatuses.Queued && Download.Status != DownloadStatuses.Downloading && Download.Status != DownloadStatuses.MassPausePending)
            {
                throw new InvalidStatusException("Invalid status, download must be Queued, Downloading or MassPausePending.");
            }

            //
            // Record operation state
            //
            StateObject = stateObject;

            //
            // Set status to Pause Pending
            //
            Download.Status = DownloadStatuses.PausePending;

            //
            // Stop all download threads
            //
            StopDownloadThreads();
        }

        /// <summary>
        /// Stops processing of the download
        /// </summary>
        public void Stop(object stateObject)
        {
            StopDownload(DownloadStatuses.Stopped, stateObject);
        }

        /// <summary>
        /// Stops processing of the download
        /// </summary>
        public void StopToQueue(object stateObject)
        {
            StopDownload(DownloadStatuses.Queued, stateObject);
        }

        /// <summary>
        /// Cancels processing of the download
        /// </summary>
        public void Cancel(object stateObject)
        {
            //
            // Check for pending operations
            //
            if (Download.Status == DownloadStatuses.CancelPending || Download.Status == DownloadStatuses.StopPending ||
                Download.Status == DownloadStatuses.PausePending || StateObject != null)
            {
                throw new OperationPendingException("An operation is pending.");
            }

            //
            // Check the download is in a valid state
            //
            if (Download.Status != DownloadStatuses.Ready && Download.Status != DownloadStatuses.Queued && Download.Status != DownloadStatuses.Downloading && Download.Status != DownloadStatuses.Stopped && Download.Status != DownloadStatuses.MassCancelPending)
            {
                throw new InvalidStatusException("Invalid status, download must be Ready, Queued, Downloading, MassCancelPending or Stopped.");
            }

            //
            // Record operation state
            //
            StateObject = stateObject;

            //
            // Set status to Pause Pending
            //
            Download.Status = DownloadStatuses.CancelPending;

            //
            // Stop all download threads
            //
            AbortDownloadThreads();
        }

        /// <summary>
        /// Aborts all threads
        /// </summary>
        public void Abort(object stateObject)
        {
            //
            // Check for pending operations
            //
            if (Download.Status == DownloadStatuses.CancelPending || Download.Status == DownloadStatuses.StopPending ||
                Download.Status == DownloadStatuses.PausePending || StateObject != null)
            {
                throw new OperationPendingException("An operation is pending.");
            }

            //
            // Check the download is in a valid state
            //
            if (Download.Status != DownloadStatuses.Paused && Download.Status != DownloadStatuses.Queued && Download.Status != DownloadStatuses.Downloading && Download.Status != DownloadStatuses.MassCancelPending)
            {
                throw new InvalidStatusException("Invalid status, download must be Paused, Queued, Downloading or MassCancelPending.");
            }

            //
            // Record operation state
            //
            StateObject = stateObject;

            //
            // Set status to Stop Pending & set stop status
            //
            Download.Status = DownloadStatuses.StopPending;

            //
            // Stop all download threads
            //
            AbortDownloadThreads();
        }

        #endregion Methods

        #region Private Methods

        /// <summary>
        /// Stops processing of the download
        /// </summary>
        private void StopDownload(DownloadStatuses stopStatus, object stateObject)
        {
            //
            // Check for pending operations
            //
            if (Download.Status == DownloadStatuses.CancelPending || Download.Status == DownloadStatuses.StopPending ||
                Download.Status == DownloadStatuses.PausePending || StateObject != null)
            {
                throw new OperationPendingException("An operation is pending.");
            }

            //
            // Check the download is in a valid state
            //
            if (Download.Status != DownloadStatuses.Paused && Download.Status != DownloadStatuses.Queued && Download.Status != DownloadStatuses.Downloading && Download.Status != DownloadStatuses.MassStopPending)
            {
                throw new InvalidStatusException("Invalid status, download must be Paused, Queued, MassStopPending or Downloading.");
            }

            //
            // Record operation state
            //
            StateObject = stateObject;

            //
            // Set status to Stop Pending & set stop status
            //
            Download.Status = DownloadStatuses.StopPending;
            StopStatus = stopStatus;

            //
            // Stop all download threads
            //
            StopDownloadThreads();
        }

        /// <summary>
        /// Ensures the download is initialized
        /// </summary>
        private Task InitializeDownload()
        {
            Task masterTask = new Task(() =>
                {
                    //
                    // Initialize download
                    //
                    if (Download.IsInitialized)
                    {
                        return;
                    }

                    //
                    // Check the download is in a valid state
                    //
                    if (Download.Status == DownloadStatuses.Downloading)
                    {
                        throw new DownloadRunningException("Invalid status, can't initialize while Downloading.");
                    }

                    //
                    // Get download factory
                    //
                    IDownloadFactory downloadFactory;
                    if (!ImportService.TryGetExportValue(DownloadPluginContractNameService.CreateDownloadFactoryContractName(Download.ParentGUID), out downloadFactory))
                    {
                        throw new Exception("The couldn't find the download factory for this download, please ensure the plugin used to create this download is loaded.");
                    }

                    while (Download.RetryCount < MaxRetries)
                    {
                        try
                        {
                            //
                            // Try initialize the download
                            //
                            Task task = downloadFactory.InitializeDownload(Download);
                            task.Wait();
                            break;
                        }
                        catch
                        {
                            //TODO Record error some where and provide feed back
                            //
                            // Increment number of retries
                            //
                            Download.RetryCount++;
                        }
                    }

                    //
                    // If number retries have been reached invoke call back handler with error
                    //
                    if (Download.RetryCount >= MaxRetries)
                    {
                        throw new RetryLimitReachedException("Retry limit (" + MaxRetries + ") reached.");
                    }
                });

            masterTask.Start();

            return masterTask;
        }

        /// <summary>
        /// Create & start download threads to try match the number of allowed threads.
        /// This method will not recalculate segments if there aren't enough to satisfy
        /// the number of allowed threads.
        /// </summary>
        private void CreateDownloadThreads()
        {
            //
            // Check the download is in a valid state
            //
            if (Download.Status != DownloadStatuses.Downloading)
            {
                throw new DownloadRunningException("Invalid status, download must be Downloading.");
            }

            //
            // Clean out any completed threads & segments
            //
            CleanDownloadThreads();

            //
            // If there are enough running threads exit
            //
            if (DownloadThreads.Count >= MaxThreads)
            {
                return;
            }

            //
            // If there are no outstanding segments set status and raise finished event
            //
            if (OutstandingSegments.Count == 0)
            {
                OnFinished();
                return;
            }

            //
            // If max number of retries have been reached and all download threads have finished
            // set the status to error and raise stopped event
            //
            if (Download.RetryCount >= MaxRetries && DownloadThreads.Count == 0)
            {
                OnStopped(new RetryLimitReachedException("Retry limit (" + MaxRetries + ") reached."), false, false, DownloadStatuses.Error);
                return;
            }

            //
            // If max number of retries has been reached exit
            //
            if (Download.RetryCount >= MaxRetries)
            {
                return;
            }

            //
            // Loop through outstanding segments creating download threads
            // until there are enough to match number of threads
            //
            foreach (DownloadSegment segment in OutstandingSegments)
            {
                //
                // Check if there is already a download thread for the segment
                //
                DownloadThread testThread;
                if (DownloadThreads.TryGetValue(segment, out testThread))
                {
                    continue;
                }

                //
                // Create new thread
                //
                DownloadThread thread = new DownloadThread(this, segment, Download);
                SubscribeToThreadEvents(thread);

                DownloadThreads.Add(segment, thread);
                thread.Start();

                //
                // If there are enough running threads exit loop
                //
                if (DownloadThreads.Count >= MaxThreads)
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Signals all download threads to stop running
        /// </summary>
        private void StopDownloadThreads()
        {
            //
            // Stop all threads
            //
            foreach (DownloadThread thread in DownloadThreads.Values)
            {
                thread.Stop();
            }
        }

        /// <summary>
        /// Signals all download threads to abort
        /// </summary>
        private void AbortDownloadThreads()
        {
            //
            // Abort all threads
            //
            foreach (DownloadThread thread in DownloadThreads.Values)
            {
                thread.Abort();
            }
        }

        /// <summary>
        /// Trims the number of running threads down to the number of allowed
        /// threads.
        /// </summary>
        private void TrimDownloadThreads()
        {
            //
            // If the number of running threads is already less than or equal to
            // the number of allowed threads exit
            //
            if (DownloadThreads.Count <= MaxThreads)
            {
                return;
            }

            //
            // Stop threads until the correct number of threads is running
            //
            int numberOfThreadsToStop = Math.Max(DownloadThreads.Values.Count - MaxThreads, 0);

            for (int i = 0; i < numberOfThreadsToStop; i++)
            {
                DownloadThread thread = DownloadThreads.Values.ElementAtOrDefault(i);

                if (thread != null)
                {
                    thread.Stop();
                }
            }
        }

        /// <summary>
        /// Cleans out all threads that are finished running & removes their segments from
        /// outstanding segments if they are completed.
        /// </summary>
        private void CleanDownloadThreads()
        {
            //
            // Loop through download threads
            //
            int count = 0;
            while (count < DownloadThreads.Count)
            {
                DownloadSegment segment = DownloadThreads.Keys.ElementAt(count);
                DownloadThread thread = DownloadThreads[segment];

                if (!thread.IsRunning)
                {
                    UnsubscribeFromThreadEvents(thread);
                    DownloadThreads.Remove(segment);

                    if (segment.IsFinished)
                    {
                        OutstandingSegments.Remove(segment);
                    }
                }
                else
                {
                    count++;
                }
            }
        }

        /// <summary>
        /// Tries to clean out all threads that are finished running & removes their segments from
        /// outstand segments if they are completed. Returns false if an unexpected error
        /// occured, otherwise true.
        /// </summary>
        private bool TryCleanDownloadThreads()
        {
            try
            {
                CleanDownloadThreads();
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Tries to match the number of running threads to the number of
        /// alowed threads. If an error occurs during this operation it'll 
        /// fail silently, this is to prevent any uncaught exceptions from 
        /// terminating the program. When logging is implemented an event 
        /// should be logged instead of the fail being silent.
        /// </summary>
        private void TryMatchDownloadThreads()
        {
            //
            // Check the download is in a valid state
            //
            if (Download.Status != DownloadStatuses.Downloading)
            {
                return;
            }

            try
            {
                MatchDownloadThreads();
            }
            catch
            { }
        }

        /// <summary>
        /// Tries to match the number of running threads to the number of
        /// alowed threads.
        /// </summary>
        private void MatchDownloadThreads()
        {
            if (DownloadThreads.Count < MaxThreads)
            {
                //
                // Recalculate segments then create new threads to match the 
                // number of allowed threads
                //
                RecalculateSegments();
                CreateDownloadThreads();
            }
            else if (DownloadThreads.Count > MaxThreads)
            {
                //
                // Trim running threads to match the number of allowed threads
                //
                TrimDownloadThreads();
            }
        }

        /// <summary>
        /// Populates the outstanding segments collection
        /// </summary>
        private void PopulateOutstandingSegments()
        {
            //
            // Clear collection and add outstanding degments from the download
            //
            OutstandingSegments.Clear();
            OutstandingSegments.AddRange(Download.Segments.Where(o => !o.IsFinished));
        }

        /// <summary>
        /// Creates segments for a download that needs to be started for the first time.
        /// </summary>
        private void CreateNewSegments()
        {
            //
            // Check download is initialized
            //
            if (!Download.IsInitialized)
            {
                throw new DownloadNotInitializedException("Download hasn't been initialized.");
            }

            //
            // Check the download is in a valid state
            //
            if (Download.Status == DownloadStatuses.Downloading)
            {
                throw new DownloadRunningException("Invalid status, can't create new threads when Downloading.");
            }

            //
            // Initialize segments list if needed
            //
            if (Download.Segments == null)
            {
                Download.Segments = new List<DownloadSegment>();
            }

            //
            // Check for existing segments
            //
            if (Download.Segments.Count > 0)
            {
                throw new DownloadInvalidSegmentStateException("Segments already exists please use the RecalculateSegments method instead.");
            }

            if (!Download.HasSize)
            {
                //
                // Add a single infinite segment. Only one segment is added because
                // downloads who's size isn't known can't be threaded.
                //
                CreateInfiniteSegment();

                //
                // Exit method
                //
                return;
            }

            if (Download.Size <= 0)
            {
                //
                // This error is thrown because the download size has to be positive if
                // HasSize is true.
                //
                throw new DownloadSizeInvalidException("Invalid download size. Download size has to be positive if HasSize is true.");
            }

            //
            // If size is smaller to equal to the buffer size add a single segment the same size as the download
            //
            if (Download.Size <= ChunkSize)
            {
                CreateSingleSegment();

                //
                // Exit method
                //
                return;
            }

            //
            // Create segments to split the file to match the number of threads
            //
            CreateSegmentsToMatchNumberOfThreads();
        }

        /// <summary>
        /// Tries to ensure there are enough segments available to satisfy the set number of threads.
        /// </summary>
        private void RecalculateSegments()
        {
            //
            // Check download is initialized
            //
            if (!Download.IsInitialized)
            {
                throw new DownloadNotInitializedException("Download hasn't been initialized.");
            }

            //
            // Check if segments have been initialized
            //
            if (Download.Segments == null)
            {
                throw new DownloadInvalidSegmentStateException("Segments haven't been properly initialized.");
            }

            //
            // If download can't has no size exit because in those cases only a single thread 
            // is needed and it should already exist
            //
            if (!Download.HasSize)
            {
                return;
            }

            //
            // Check for existing segments, downloads should never reached this point with out having
            // segments created.
            //
            if (Download.Segments.Count <= 0)
            {
                throw new DownloadInvalidSegmentStateException("Segments haven't been properly created.");
            }

            //
            // Check if there are already enough segments or if there are no outstanding segments
            // left to split exit.
            //
            long outstandingSegmentCount = Download.Segments.LongCount(o => !o.IsFinished);
            if (outstandingSegmentCount >= MaxThreads || outstandingSegmentCount <= 0)
            {
                return;
            }

            //
            // Split segments to try produce enough to satisfy all threads
            //
            bool loop = true;
            while (loop)
            {
                bool wereSegmentsSplit = false;

                //
                // Loop through exising segments splitting them
                //
                foreach (DownloadSegment segment in Download.Segments.ToList())
                {
                    if (SplitSegemnt(segment))
                    {
                        wereSegmentsSplit = true;
                    }
                }

                //
                // Exit loop if there are enough segments to satisfy all threads or if no segments where split
                //
                loop = Download.Segments.LongCount(o => !o.IsFinished) < MaxThreads && wereSegmentsSplit;
            }

            //
            // Populate the outstanding segments collection
            //
            PopulateOutstandingSegments();
        }

        /// <summary>
        ///  Tries to split the passed segment
        /// </summary>
        /// <returns>True if segment was split otherwise false.</returns>
        private bool SplitSegemnt(DownloadSegment segment)
        {
            //
            // If segment is infinite or has completed downloading exit
            //
            if (segment.IsFinished || segment.IsInfinite)
            {
                return false;
            }

            //
            // If size remaining on segment is to small don't split.
            //
            if (segment.SizeRemaining < Download.ChunkSize * 10 || segment.SizeRemaining <= 0)
            {
                return false;
            }

            //
            // Reduce the remaining size of the segment by half
            //
            long sizeRemaining = segment.SizeRemaining;
            long endPosition = segment.EndPosition;
            
            segment.EndPosition -= sizeRemaining / 2;

            //
            // Create new segment
            //
            long newStartPosition = endPosition - (sizeRemaining / 2) + 1;
            DownloadSegment newSegment = new DownloadSegment
            {
                StartPosition = newStartPosition,
                CurrentPosition = newStartPosition,
                EndPosition = endPosition
            };

            //
            // Add new segment to the download
            //
            Download.Segments.Add(newSegment);

            return true;
        }

        /// <summary>
        /// Creates an infinite segment
        /// </summary>
        private void CreateInfiniteSegment()
        {
            //
            // Clear any existing segments, this is done because only 1 infinite segment should be allowed to exist.
            //
            Download.Segments.Clear();

            //
            // Add the segment
            //
            Download.Segments.Add(new DownloadSegment
            {
                StartPosition = 0,
                CurrentPosition = 0,
                EndPosition = -1,
            });
        }

        /// <summary>
        /// Creates a single segment the same size as the download
        /// </summary>
        private void CreateSingleSegment()
        {
            //
            // Clear any existing segments, this is done because the segment to be added will cover the entire size of the download.
            //
            Download.Segments.Clear();

            //
            // Add the segment
            //
            Download.Segments.Add(new DownloadSegment
            {
                StartPosition = 0,
                CurrentPosition = 0,
                EndPosition = Download.Size - 1,
            });
        }

        /// <summary>
        /// Creates segments which split the download into the same number of parts as number of threads 
        /// </summary>
        private void CreateSegmentsToMatchNumberOfThreads()
        {
            //
            // Clear any existing segments, this is done because the sum of all the segments will cover the size of download.
            //
            Download.Segments.Clear();

            //
            // Calculate Segment Size and Count
            //
            long segmentCount = (Download.HasSize && Download.CanResume) ? Math.Max(1, (MaxThreads)) : 1;

            long segmentSize = Download.Size / segmentCount;

            //
            // Add Segments
            //
            for (int i = 0; i < segmentCount; i++)
            {
                //
                // Get the size of the current segment to add, when on the last segment this 
                //
                long currentSegmentSize = (i < segmentCount - 1) ?
                    segmentSize :
                    Download.Size - (segmentSize * i);

                Download.Segments.Add(new DownloadSegment
                {
                    StartPosition = i * segmentSize,
                    CurrentPosition = i * segmentSize,
                    EndPosition = (i * segmentSize) + currentSegmentSize - 1,
                });
            }
        }

        /// <summary>
        /// Generates a unique destination path based off the destination folder and file name
        /// </summary>
        private string GetUniqueDestinationPath()
        {
            //
            // Check destination folder exists
            //
            DirectoryInfo di = new DirectoryInfo(Download.DestinationFolder);
            if (!di.Exists)
            {
                throw new IOException("Folder '" + Download.DestinationFolder + "' doesn't exits.");
            }

            //
            // Create unique path
            //
            FileInfo fi = new FileInfo(Path.Combine(Download.DestinationFolder, Download.FileName));

            int count = 1;
            while (fi.Exists)
            {
                string newFileName = string.Format(Path.GetFileNameWithoutExtension(fi.FullName) + "({0})", count);
                newFileName += Path.GetExtension(fi.FullName);

                fi = new FileInfo(Path.Combine(fi.DirectoryName, newFileName));
                count++;

                if (count > 100000)
                {
                    throw new IOException("Couldn't find unique file name after 100 000 attempts.");
                }
            }

            return fi.FullName;
        }

        /// <summary>
        /// Subscribes to a threads stopped and ChunkProcessed events
        /// </summary>
        private void SubscribeToThreadEvents(DownloadThread thread)
        {
            //
            // TODO Subscribe to thread started events so that when they occur it can be logged
            //
            thread.Stopped -= ThreadStopped;
            thread.Stopped += ThreadStopped;

            thread.ChunkProcessed -= ThreadChunkProcessed;
            thread.ChunkProcessed += ThreadChunkProcessed;
        }

        /// <summary>
        /// Unsubscribes from a threads stopped and ChunkProcessed events
        /// </summary>
        private void UnsubscribeFromThreadEvents(DownloadThread thread)
        {
            thread.Stopped -= ThreadStopped;
            thread.ChunkProcessed -= ThreadChunkProcessed;
        }

        /// <summary>
        /// Subscribes to the elapsed timer. If the timer isn't running it's started
        /// </summary>
        private void SubscribeToElapsedTimer()
        {
            //
            // Subscribe
            //
            ElapsedTimer.Tick += ElapsedTimerTick;
            ElapsedTimerSubscriptionCount++;

            //
            // Start timer if it isn't running
            //
            if (!ElapsedTimer.IsEnabled)
            {
                ElapsedTimer.Start();
            }
        }

        /// <summary>
        /// Unsubscribes from the elapsed timer. If there aren'y anymore subscriptions
        /// on the timers tick event the timer is stopped
        /// </summary>
        private void UnsubscribeToElapsedTimer()
        {
            //
            // Unsubscribe
            //
            ElapsedTimer.Tick -= ElapsedTimerTick;
            ElapsedTimerSubscriptionCount--;

            //
            // Stop timer if there aren't any subscriptions on the tick event
            //
            if (ElapsedTimerSubscriptionCount == 0)
            {
                ElapsedTimer.Stop();
            }
        }

        /// <summary>
        /// Updates the DownloadedAmount for the download
        /// </summary>
        /// <param name="amount">The amount to add to the downloaded amount.</param>
        /// <param name="forceUpdate">Indicates that the downloaded amount should be updated regardless of the current status or last update date time.</param>
        private void UpdateDownloadedAmount(long amount, bool forceUpdate)
        {
            DownloadedAmountBuffer += amount;

            if (Download.Status == DownloadStatuses.Downloading || forceUpdate)
            {
                if ((DateTime.Now - DownloadedAmountLastUpdate).TotalSeconds > 1 || forceUpdate)
                {
                    Download.DownloadedAmount += DownloadedAmountBuffer;
                    DownloadedAmountBuffer = 0;
                    DownloadedAmountLastUpdate = DateTime.Now;
                }
            }
        }

        #endregion Private Methods

        #region Thread Event Handlers

        /// <summary>
        /// Handles when a thread stops
        /// </summary>
        private void ThreadStopped(object sender, DownloadThreadStoppedEventArgs e)
        {
            //
            // Check if thread stopped with an error
            //
            if (e.Error != null)
            {
                //
                // Increment number of retries on the download
                //
                Download.RetryCount++;
            }

            //
            // Catch any problems that might happen while performing thread & segment
            // maintenence. This needs to be here because this code is executed
            // from a call which is marsheld to the UI thread form inside a download
            // thread, therefore the exceptions raised here will be raised on the download thread 
            // and won't be caught by try/catches that surrounding the methods which started the 
            // process initially.
            //
            try
            {
                //
                // React based on status
                //
                if (Download.Status == DownloadStatuses.Downloading)
                {
                    if (e.BandwidthDepleted)
                    {
                        //
                        // Clean threads and segments
                        //
                        CleanDownloadThreads();

                        //
                        // Ensure that any figure remaining in the downloaded amount buffer is added before the processor pauses
                        //
                        UpdateDownloadedAmount(0, true);

                        //
                        // Pause download
                        //
                        Pause(StateObject);
                    }
                    else
                    {
                        //
                        // Create new downloads to take the place of the one which has stopped
                        //
                        CreateDownloadThreads();
                    }
                }
                else if (Download.Status == DownloadStatuses.PausePending)
                {
                    //
                    // Clean threads and segments
                    //
                    CleanDownloadThreads();

                    //
                    // Set status to Paused & raise paused event if all threads have successfully stopped
                    //
                    if (DownloadThreads.Count == 0)
                    {
                        OnPaused();
                    }
                }
                else if (Download.Status == DownloadStatuses.StopPending)
                {
                    //
                    // Clean threads and segments
                    //
                    CleanDownloadThreads();

                    //
                    // Set status to Stopped and raise stopped event if all threads have successfully stopped
                    //
                    if (DownloadThreads.Count == 0)
                    {
                        OnStopped(null, false, false, StopStatus);
                    }
                }
                else if (Download.Status == DownloadStatuses.CancelPending)
                {
                    //
                    // Clean threads and segments
                    //
                    CleanDownloadThreads();

                    //
                    // Set status to Stopped and raise stopped event if all threads have successfully stopped
                    //
                    if (DownloadThreads.Count == 0)
                    {
                        OnStopped(null, false, true, DownloadStatuses.Canceled);
                    }
                }
                else
                {
                    throw new InvalidStatusException("Unexpected status on thread stop. '" + Download.Status + " recieved, Downloading, PausePending, StopPending or CancelPending expected.");
                }
            }
            catch (Exception)
            {
                //
                // TODO Imlement some mechanism to get information out of here so that it can be logged
                //
                //
                // Increment number of retries on the download
                //
                Download.RetryCount++;

                //
                // Try clean
                //
                TryCleanDownloadThreads();

                //
                // If max number of retries have been reached and all download threads have finished
                // set the status to error and raise stopped event. The numbe rof running threads is 
                // calculated independantly incase there are errors running the clean.
                //
                long runningThreadCount = DownloadThreads.Count(d => !d.Value.IsRunning);
                if (Download.RetryCount >= MaxRetries && runningThreadCount == 0)
                {
                    OnStopped(new RetryLimitReachedException("Retry limit (" + MaxRetries + ") reached."), false, false, DownloadStatuses.Error);
                }
            }
        }

        /// <summary>
        /// Handles when a thread has successfully processed a chunk
        /// </summary>
        private void ThreadChunkProcessed(object sender, DownloadThreadChunkProcessedEventArgs e)
        {
            UpdateDownloadedAmount(e.ChunkSize, false);
        }

        #endregion Thread Event Handlers

        #region Elapsed Timer Event Handlers

        /// <summary>
        /// Handles the tick of the elapsed timer
        /// </summary>
        private void ElapsedTimerTick(object sender, EventArgs e)
        {
            if (Download.Status == DownloadStatuses.Downloading)
            {
                Download.TimeElapsed += new TimeSpan(0, 0, 1);
            }            
        }

        #endregion Elapsed Timer Event Handlers
    }
}
